// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "media/remoting/remoting_renderer_controller.h"

#include "base/bind.h"
#include "base/logging.h"
#include "base/threading/thread_checker.h"
#include "base/time/time.h"
#include "media/remoting/remoting_cdm_context.h"

namespace media {

RemotingRendererController::RemotingRendererController(
    scoped_refptr<RemotingSourceImpl> remoting_source)
    : remoting_source_(remoting_source)
    , weak_factory_(this)
{
    remoting_source_->AddClient(this);
}

RemotingRendererController::~RemotingRendererController()
{
    DCHECK(thread_checker_.CalledOnValidThread());
    metrics_recorder_.WillStopSession(remoting::MEDIA_ELEMENT_DESTROYED);
    remoting_source_->RemoveClient(this);
}

void RemotingRendererController::OnStarted(bool success)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    if (success) {
        VLOG(1) << "Remoting started successively.";
        if (remote_rendering_started_) {
            metrics_recorder_.DidStartSession();
            DCHECK(!switch_renderer_cb_.is_null());
            switch_renderer_cb_.Run();
        } else {
            remoting_source_->StopRemoting(this);
        }
    } else {
        VLOG(1) << "Failed to start remoting.";
        remote_rendering_started_ = false;
        metrics_recorder_.WillStopSession(remoting::START_RACE);
    }
}

void RemotingRendererController::OnSessionStateChanged()
{
    DCHECK(thread_checker_.CalledOnValidThread());
    UpdateFromSessionState(remoting::SINK_AVAILABLE, remoting::ROUTE_TERMINATED);
}

void RemotingRendererController::UpdateFromSessionState(
    remoting::StartTrigger start_trigger,
    remoting::StopTrigger stop_trigger)
{
    VLOG(1) << "UpdateFromSessionState: " << remoting_source_->state();
    if (!sink_available_changed_cb_.is_null())
        sink_available_changed_cb_.Run(IsRemoteSinkAvailable());

    UpdateInterstitial(base::nullopt);
    UpdateAndMaybeSwitch(start_trigger, stop_trigger);
}

bool RemotingRendererController::IsRemoteSinkAvailable()
{
    DCHECK(thread_checker_.CalledOnValidThread());

    switch (remoting_source_->state()) {
    case SESSION_CAN_START:
    case SESSION_STARTING:
    case SESSION_STARTED:
        return true;
    case SESSION_UNAVAILABLE:
    case SESSION_STOPPING:
    case SESSION_PERMANENTLY_STOPPED:
        return false;
    }

    return false; // To suppress compile warning.
}

void RemotingRendererController::OnEnteredFullscreen()
{
    DCHECK(thread_checker_.CalledOnValidThread());

    is_fullscreen_ = true;
    // See notes in OnBecameDominantVisibleContent() for why this is forced:
    is_dominant_content_ = true;
    UpdateAndMaybeSwitch(remoting::ENTERED_FULLSCREEN,
        remoting::UNKNOWN_STOP_TRIGGER);
}

void RemotingRendererController::OnExitedFullscreen()
{
    DCHECK(thread_checker_.CalledOnValidThread());

    is_fullscreen_ = false;
    // See notes in OnBecameDominantVisibleContent() for why this is forced:
    is_dominant_content_ = false;
    UpdateAndMaybeSwitch(remoting::UNKNOWN_START_TRIGGER,
        remoting::EXITED_FULLSCREEN);
}

void RemotingRendererController::OnBecameDominantVisibleContent(
    bool is_dominant)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    // Two scenarios where "dominance" status mixes with fullscreen transitions:
    //
    //   1. Just before/after entering fullscreen, the element will, of course,
    //      become the dominant on-screen content via automatic page layout.
    //   2. Just before/after exiting fullscreen, the element may or may not
    //      shrink in size enough to become non-dominant. However, exiting
    //      fullscreen was caused by a user action that explicitly indicates a
    //      desire to exit remoting, so even if the element is still dominant,
    //      remoting should be shut down.
    //
    // Thus, to achieve the desired behaviors, |is_dominant_content_| is force-set
    // in OnEnteredFullscreen() and OnExitedFullscreen(), and changes to it here
    // are ignored while in fullscreen.
    if (is_fullscreen_)
        return;

    is_dominant_content_ = is_dominant;
    UpdateAndMaybeSwitch(remoting::BECAME_DOMINANT_CONTENT,
        remoting::BECAME_AUXILIARY_CONTENT);
}

void RemotingRendererController::OnSetCdm(CdmContext* cdm_context)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    auto* remoting_cdm_context = RemotingCdmContext::From(cdm_context);
    if (!remoting_cdm_context)
        return;

    remoting_source_->RemoveClient(this);
    remoting_source_ = remoting_cdm_context->GetRemotingSource();
    remoting_source_->AddClient(this);
    UpdateFromSessionState(remoting::CDM_READY, remoting::DECRYPTION_ERROR);
}

void RemotingRendererController::OnRemotePlaybackDisabled(bool disabled)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    is_remote_playback_disabled_ = disabled;
    metrics_recorder_.OnRemotePlaybackDisabled(disabled);
    UpdateAndMaybeSwitch(remoting::ENABLED_BY_PAGE, remoting::DISABLED_BY_PAGE);
}

void RemotingRendererController::OnSetPoster(const GURL& poster_url)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    if (poster_url != poster_url_) {
        poster_url_ = poster_url;
        if (poster_url_.is_empty())
            UpdateInterstitial(SkBitmap());
        else
            DownloadPosterImage();
    }
}

void RemotingRendererController::SetSwitchRendererCallback(
    const base::Closure& cb)
{
    DCHECK(thread_checker_.CalledOnValidThread());
    DCHECK(!cb.is_null());

    switch_renderer_cb_ = cb;
    // Note: Not calling UpdateAndMaybeSwitch() here since this method should be
    // called during the initialization phase of this RemotingRendererController;
    // and definitely before a whole lot of other things that are needed to
    // trigger a switch.
}

void RemotingRendererController::SetRemoteSinkAvailableChangedCallback(
    const base::Callback<void(bool)>& cb)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    sink_available_changed_cb_ = cb;
    if (!sink_available_changed_cb_.is_null())
        sink_available_changed_cb_.Run(IsRemoteSinkAvailable());
}

base::WeakPtr<remoting::RpcBroker> RemotingRendererController::GetRpcBroker()
    const
{
    DCHECK(thread_checker_.CalledOnValidThread());

    return remoting_source_->GetRpcBroker()->GetWeakPtr();
}

void RemotingRendererController::StartDataPipe(
    std::unique_ptr<mojo::DataPipe> audio_data_pipe,
    std::unique_ptr<mojo::DataPipe> video_data_pipe,
    const RemotingSourceImpl::DataPipeStartCallback& done_callback)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    remoting_source_->StartDataPipe(std::move(audio_data_pipe),
        std::move(video_data_pipe), done_callback);
}

void RemotingRendererController::OnMetadataChanged(
    const PipelineMetadata& metadata)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    const gfx::Size old_size = pipeline_metadata_.natural_size;
    const bool was_audio_codec_supported = has_audio() && IsAudioCodecSupported();
    const bool was_video_codec_supported = has_video() && IsVideoCodecSupported();
    pipeline_metadata_ = metadata;
    const bool is_audio_codec_supported = has_audio() && IsAudioCodecSupported();
    const bool is_video_codec_supported = has_video() && IsVideoCodecSupported();
    metrics_recorder_.OnPipelineMetadataChanged(metadata);

    is_encrypted_ = false;
    if (has_video())
        is_encrypted_ |= metadata.video_decoder_config.is_encrypted();
    if (has_audio())
        is_encrypted_ |= metadata.audio_decoder_config.is_encrypted();

    if (pipeline_metadata_.natural_size != old_size)
        UpdateInterstitial(base::nullopt);

    remoting::StartTrigger start_trigger = remoting::UNKNOWN_START_TRIGGER;
    if (!was_audio_codec_supported && is_audio_codec_supported)
        start_trigger = remoting::SUPPORTED_AUDIO_CODEC;
    if (!was_video_codec_supported && is_video_codec_supported) {
        start_trigger = start_trigger == remoting::SUPPORTED_AUDIO_CODEC
            ? remoting::SUPPORTED_AUDIO_AND_VIDEO_CODECS
            : remoting::SUPPORTED_VIDEO_CODEC;
    }
    remoting::StopTrigger stop_trigger = remoting::UNKNOWN_STOP_TRIGGER;
    if (was_audio_codec_supported && !is_audio_codec_supported)
        stop_trigger = remoting::UNSUPPORTED_AUDIO_CODEC;
    if (was_video_codec_supported && !is_video_codec_supported) {
        stop_trigger = stop_trigger == remoting::UNSUPPORTED_AUDIO_CODEC
            ? remoting::UNSUPPORTED_AUDIO_AND_VIDEO_CODECS
            : remoting::UNSUPPORTED_VIDEO_CODEC;
    }
    UpdateAndMaybeSwitch(start_trigger, stop_trigger);
}

bool RemotingRendererController::IsVideoCodecSupported()
{
    DCHECK(thread_checker_.CalledOnValidThread());
    DCHECK(has_video());

    switch (pipeline_metadata_.video_decoder_config.codec()) {
    case VideoCodec::kCodecH264:
    case VideoCodec::kCodecVP8:
        return true;
    default:
        VLOG(2) << "Remoting does not support video codec: "
                << pipeline_metadata_.video_decoder_config.codec();
        return false;
    }
}

bool RemotingRendererController::IsAudioCodecSupported()
{
    DCHECK(thread_checker_.CalledOnValidThread());
    DCHECK(has_audio());

    switch (pipeline_metadata_.audio_decoder_config.codec()) {
    case AudioCodec::kCodecAAC:
    case AudioCodec::kCodecMP3:
    case AudioCodec::kCodecPCM:
    case AudioCodec::kCodecVorbis:
    case AudioCodec::kCodecFLAC:
    case AudioCodec::kCodecAMR_NB:
    case AudioCodec::kCodecAMR_WB:
    case AudioCodec::kCodecPCM_MULAW:
    case AudioCodec::kCodecGSM_MS:
    case AudioCodec::kCodecPCM_S16BE:
    case AudioCodec::kCodecPCM_S24BE:
    case AudioCodec::kCodecOpus:
    case AudioCodec::kCodecEAC3:
    case AudioCodec::kCodecPCM_ALAW:
    case AudioCodec::kCodecALAC:
    case AudioCodec::kCodecAC3:
        return true;
    default:
        VLOG(2) << "Remoting does not support audio codec: "
                << pipeline_metadata_.audio_decoder_config.codec();
        return false;
    }
}

void RemotingRendererController::OnPlaying()
{
    DCHECK(thread_checker_.CalledOnValidThread());

    is_paused_ = false;
    UpdateAndMaybeSwitch(remoting::PLAY_COMMAND, remoting::UNKNOWN_STOP_TRIGGER);
}

void RemotingRendererController::OnPaused()
{
    DCHECK(thread_checker_.CalledOnValidThread());

    is_paused_ = true;
}

bool RemotingRendererController::ShouldBeRemoting()
{
    DCHECK(thread_checker_.CalledOnValidThread());

    if (switch_renderer_cb_.is_null()) {
        DCHECK(!remote_rendering_started_);
        return false; // No way to switch to a RemotingRenderImpl.
    }

    const RemotingSessionState state = remoting_source_->state();
    if (is_encrypted_) {
        // Due to technical limitations when playing encrypted content, once a
        // remoting session has been started, always return true here to indicate
        // that the RemotingRendererImpl should be used. In the stopped states,
        // RemotingRendererImpl will display an interstitial to notify the user that
        // local rendering cannot be resumed.
        //
        // TODO(miu): Revisit this once more of the encrypted-remoting impl is
        // in-place. For example, this will prevent metrics from recording session
        // stop reasons.
        return state == RemotingSessionState::SESSION_STARTED || state == RemotingSessionState::SESSION_STOPPING || state == RemotingSessionState::SESSION_PERMANENTLY_STOPPED;
    }

    if (encountered_renderer_fatal_error_)
        return false;

    switch (state) {
    case SESSION_UNAVAILABLE:
        return false; // Cannot remote media without a remote sink.
    case SESSION_CAN_START:
    case SESSION_STARTING:
    case SESSION_STARTED:
        break; // Media remoting is possible, assuming other requirments are met.
    case SESSION_STOPPING:
    case SESSION_PERMANENTLY_STOPPED:
        return false; // Use local rendering after stopping remoting.
    }

    switch (remoting_source_->sink_capabilities()) {
    case mojom::RemotingSinkCapabilities::NONE:
        return false;
    case mojom::RemotingSinkCapabilities::RENDERING_ONLY:
    case mojom::RemotingSinkCapabilities::CONTENT_DECRYPTION_AND_RENDERING:
        break; // The sink is capable of remote rendering.
    }

    if ((!has_audio() && !has_video()) || (has_video() && !IsVideoCodecSupported()) || (has_audio() && !IsAudioCodecSupported())) {
        return false;
    }

    if (is_remote_playback_disabled_)
        return false;

    // Normally, entering fullscreen or being the dominant visible content is the
    // signal that starts remote rendering. However, current technical limitations
    // require encrypted content be remoted without waiting for a user signal.
    return is_fullscreen_ || is_dominant_content_;
}

void RemotingRendererController::UpdateAndMaybeSwitch(
    remoting::StartTrigger start_trigger,
    remoting::StopTrigger stop_trigger)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    bool should_be_remoting = ShouldBeRemoting();

    if (remote_rendering_started_ == should_be_remoting)
        return;

    // Only switch to remoting when media is playing. Since the renderer is
    // created when video starts loading/playing, receiver will display a black
    // screen before video starts playing if switching to remoting when paused.
    // Thus, the user experience is improved by not starting remoting until
    // playback resumes.
    if (should_be_remoting && is_paused_)
        return;

    // Switch between local renderer and remoting renderer.
    remote_rendering_started_ = should_be_remoting;

    if (remote_rendering_started_) {
        DCHECK(!switch_renderer_cb_.is_null());
        if (remoting_source_->state() == RemotingSessionState::SESSION_PERMANENTLY_STOPPED) {
            switch_renderer_cb_.Run();
            return;
        }
        DCHECK_NE(start_trigger, remoting::UNKNOWN_START_TRIGGER);
        metrics_recorder_.WillStartSession(start_trigger);
        // |switch_renderer_cb_.Run()| will be called after remoting is started
        // successfully.
        remoting_source_->StartRemoting(this);
    } else {
        // For encrypted content, it's only valid to switch to remoting renderer,
        // and never back to the local renderer. The RemotingCdmController will
        // force-stop the session when remoting has ended; so no need to call
        // StopRemoting() from here.
        DCHECK(!is_encrypted_);
        DCHECK_NE(stop_trigger, remoting::UNKNOWN_STOP_TRIGGER);
        metrics_recorder_.WillStopSession(stop_trigger);
        // Update the interstitial one last time before switching back to the local
        // Renderer.
        UpdateInterstitial(base::nullopt);
        switch_renderer_cb_.Run();
        remoting_source_->StopRemoting(this);
    }
}

void RemotingRendererController::SetShowInterstitialCallback(
    const ShowInterstitialCallback& cb)
{
    DCHECK(thread_checker_.CalledOnValidThread());
    show_interstitial_cb_ = cb;
    UpdateInterstitial(SkBitmap());
    if (!poster_url_.is_empty())
        DownloadPosterImage();
}

void RemotingRendererController::SetDownloadPosterCallback(
    const DownloadPosterCallback& cb)
{
    DCHECK(thread_checker_.CalledOnValidThread());
    DCHECK(download_poster_cb_.is_null());
    download_poster_cb_ = cb;
    if (!poster_url_.is_empty())
        DownloadPosterImage();
}

void RemotingRendererController::UpdateInterstitial(
    const base::Optional<SkBitmap>& image)
{
    DCHECK(thread_checker_.CalledOnValidThread());
    if (show_interstitial_cb_.is_null())
        return;

    RemotingInterstitialType type = RemotingInterstitialType::BETWEEN_SESSIONS;
    switch (remote_rendering_started_ ? remoting_source_->state()
                                      : SESSION_STOPPING) {
    case SESSION_STARTED:
        type = RemotingInterstitialType::IN_SESSION;
        break;
    case SESSION_PERMANENTLY_STOPPED:
        type = RemotingInterstitialType::ENCRYPTED_MEDIA_FATAL_ERROR;
        break;
    case SESSION_UNAVAILABLE:
    case SESSION_CAN_START:
    case SESSION_STARTING:
    case SESSION_STOPPING:
        break;
    }

    bool needs_update = false;
    if (image.has_value()) {
        interstitial_background_ = image.value();
        needs_update = true;
    }
    if (interstitial_natural_size_ != pipeline_metadata_.natural_size) {
        interstitial_natural_size_ = pipeline_metadata_.natural_size;
        needs_update = true;
    }
    if (interstitial_type_ != type) {
        interstitial_type_ = type;
        needs_update = true;
    }
    if (!needs_update)
        return;

    show_interstitial_cb_.Run(interstitial_background_,
        interstitial_natural_size_, interstitial_type_);
}

void RemotingRendererController::DownloadPosterImage()
{
    if (download_poster_cb_.is_null() || show_interstitial_cb_.is_null())
        return;
    DCHECK(!poster_url_.is_empty());

    const base::TimeTicks download_start_time = base::TimeTicks::Now();
    download_poster_cb_.Run(
        poster_url_,
        base::Bind(&RemotingRendererController::OnPosterImageDownloaded,
            weak_factory_.GetWeakPtr(), poster_url_, download_start_time));
}

void RemotingRendererController::OnPosterImageDownloaded(
    const GURL& download_url,
    base::TimeTicks download_start_time,
    const SkBitmap& image)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    metrics_recorder_.OnPosterImageDownloaded(
        base::TimeTicks::Now() - download_start_time, !image.drawsNothing());
    if (download_url != poster_url_)
        return; // The poster image URL has changed during the download.
    UpdateInterstitial(image);
}

void RemotingRendererController::OnRendererFatalError(
    remoting::StopTrigger stop_trigger)
{
    DCHECK(thread_checker_.CalledOnValidThread());

    // Do not act on errors caused by things like Mojo pipes being closed during
    // shutdown.
    if (!remote_rendering_started_)
        return;

    encountered_renderer_fatal_error_ = true;
    UpdateAndMaybeSwitch(remoting::UNKNOWN_START_TRIGGER, stop_trigger);
}

} // namespace media
